﻿using System.IO;
using gixoom.Dom.Attributes;
using gixoom.Dom.Block;
using gixoom.Dom.Storage.Block;

namespace gixoom.Dom.Storage.File
{
    internal class FileStorage: IFileStorage
    {
        private string pathForExtractedFiles;
        private int defaultBlockSizeBytes;
        private IBlockFacade blockFacade;
        private IBlockIdToFileNameMapping blockIdToFileNameMapping;
        private IBlockStorage blockStorage;
        private int defaultBlockSizeBytesMinusOne;

        /// <summary>
        /// Creates FileStorage object.
        /// </summary>
        /// <param name="defaultBlockSizeBytes">default block size (the number of bytes)</param>
        /// <param name="blockFacade">block facade</param>
        /// <param name="blockIdToFileNameMapping">block id to file name mapping</param>
        /// <param name="blockStorage">block storage</param>
        /// <exception cref="System.ArgumentOutOfRangeException"><i>defaultBlockSizeBytes</i>
        /// is less than or equal to zero</exception>
        /// <exception cref="System.ArgumentNullException">One of the arguments is a null reference.</exception>
        public FileStorage([DefaultBlockSizeBytes] int defaultBlockSizeBytes, IBlockFacade blockFacade,
            IBlockIdToFileNameMapping blockIdToFileNameMapping, IBlockStorage blockStorage)
        {
            Ensure.IntGreaterThanZero(defaultBlockSizeBytes, "defaultBlockSizeBytes");
            Ensure.ArgumentNotNull(blockFacade, "blockFacade");
            Ensure.ArgumentNotNull(blockIdToFileNameMapping, "blockIdToFileNameMapping");
            Ensure.ArgumentNotNull(blockStorage, "blockStorage");

            this.defaultBlockSizeBytes = defaultBlockSizeBytes;
            this.blockFacade = blockFacade;
            this.blockIdToFileNameMapping = blockIdToFileNameMapping;
            this.blockStorage = blockStorage;

            defaultBlockSizeBytesMinusOne = defaultBlockSizeBytes - 1;
        }

        /// <summary>
        /// Extracts file which corresponds to the specified root block id.
        /// </summary>
        /// <param name="rootBlockId">root block id of the file</param>
        /// <returns>name of the file extracted from storage or null if there is no such file
        /// or if there is no such file name in dictionary.</returns>
        /// <exception cref="System.ArgumentNullException"><i>rootBlockId</i> is a null reference.</exception>
        public string ExtractFile(IBlockId rootBlockId)
        {
            string name = blockIdToFileNameMapping.GetFileNameByBlockId(rootBlockId);

            if (name != null)
            {
                name = FormFullName(pathForExtractedFiles, name);
                FileStream stream = CreateFile(name);

                if (stream == null)
                {
                    name = null;
                }
                else
                {
                    bool isSuccessfull;

                    try
                    {
                        isSuccessfull = ExtractBlocksToFile(rootBlockId, stream);
                    }
                    finally
                    {
                        stream.Close();
                    }

                    if (!isSuccessfull)
                    {
                        DeleteFile(name);
                        name = null;
                    }
                }
            }

            return name;
        }

        /// <summary>
        /// Saves file from the specified file stream to storage and returns its root block id.
        /// If for some reason the file cannot be saved in the storage than the function returns null.
        /// Stream is read from beginning, not from current position.
        /// </summary>
        /// <param name="fileStream">opened file stream</param>
        /// <returns>root block id of the file saved in the storage</returns>
        /// <exception cref="System.ArgumentNullException"><i>fileStream</i> is a null reference.</exception>
        /// <exception cref="System.ArgumentException"><i>fileStream</i> cannot be read or sought.</exception>
        public IBlockId SaveFile(FileStream fileStream)
        {
            Ensure.ArgumentNotNull(fileStream, "fileStream");
            Ensure.StreamCanBeRead(fileStream, "fileStream");
            Ensure.StreamCanBeSought(fileStream, "fileStream");

            // TODO: Stream and FileStream should be wrapped.
            Stream stream = fileStream;
            byte[] content = new byte[defaultBlockSizeBytes];
            int blockIdSizeBytes = blockFacade.BlockIdSizeBytes;
            BlockType blockType = BlockType.Data;
            IBlock block = null;

            do
            {
                var blockIdsBytes = new byte[(stream.Length + defaultBlockSizeBytesMinusOne) /
                    defaultBlockSizeBytes * blockIdSizeBytes];
                int blockIdsBytesIndex = 0;

                // TODO: specific exceptions only should be catched and correctly processed.
                try
                {
                    stream.Seek(0, SeekOrigin.Begin);
                    int bytesRead;

                    do
                    {
                        bytesRead = stream.Read(content, 0, defaultBlockSizeBytes);

                        if (bytesRead > 0)
                        {
                            block = blockFacade.CreateBlockFromContent(blockType, content,
                                0, bytesRead);

                            if (!blockStorage.ExistsBlock(block.Id))
                            {
                                bool isSuccessfull = blockStorage.StoreBlock(block);

                                if (!isSuccessfull)
                                {
                                    return null;
                                }
                            }

                            blockIdsBytesIndex = blockFacade.SerializeBlockId(block.Id,
                                blockIdsBytes, blockIdsBytesIndex);
                        }
                    }
                    while (bytesRead == defaultBlockSizeBytes);
                }
                catch
                {
                    return null;
                }

                if (blockIdsBytes.Length == blockIdSizeBytes)
                {
                    break;
                }

                blockType = BlockType.Pointer;
                stream = new MemoryStream(blockIdsBytes);
            }
            while (true);

            string fileName = GetFileNameFromPath(fileStream.Name);
            blockIdToFileNameMapping.SetFileNameByBlockId(fileName, block.Id);

            return block.Id;
        }

        /// <summary>
        /// Sets path for files extracted from storage.
        /// </summary>
        /// <param name="path">path for files extracted from storage</param>
        /// <exception cref="System.ArgumentNullException"><i>path</i> is a null reference.</exception>
        /// <exception cref="System.ArgumentException"><i>path</i> is an empty string.</exception>
        public void SetPathForExtractedFiles(string path)
        {
            Ensure.ArgumentNotNull(path, "path");
            Ensure.StringArgumentNotEmpty(path, "path");

            this.pathForExtractedFiles = path;
        }

        /// <summary>
        /// Gets path for files extracted from storage.
        /// </summary>
        public string PathForExtractedFiles
        {
            get
            {
                return pathForExtractedFiles;
            }
        }

        private FileStream CreateFile(string name)
        {
            FileStream stream = null;

            // TODO: exceptions should be properly processed
            try
            {
                // TODO: the user should be provided with an option wether the file
                // should be overwritten in case the file with the same name already exists.
                stream = System.IO.File.Create(name);
            }
            catch
            {
            }

            return stream;
        }

        private void DeleteFile(string name)
        {
            // TODO: exceptions should be properly processed
            try
            {
                System.IO.File.Delete(name);
            }
            catch
            {
            }
        }

        private bool ExtractBlocksToFile(IBlockId blockId, FileStream stream)
        {
            bool isSuccessfull = true;
            IBlock block = blockStorage.RetrieveBlock(blockId);

            isSuccessfull = (block != null);

            if (isSuccessfull)
            {
                byte[] content = block.GetContentCopy();

                if (blockId.BlockType == BlockType.Data)
                {
                    stream.Write(content, 0, content.Length);
                }
                else if (blockId.BlockType == BlockType.Pointer)
                {
                    var blockIdSizeBytes = blockFacade.BlockIdSizeBytes;
                    var contentLength = content.Length;

                    for (int i = 0; i < contentLength && isSuccessfull; i += blockIdSizeBytes)
                    {
                        blockId = blockFacade.CreateBlockIdFromBytes(content, i);
                        isSuccessfull = ExtractBlocksToFile(blockId, stream);
                    }
                }
            }

            return isSuccessfull;
        }

        private string FormFullName(string path, string fileName)
        {
            string fullName = path[path.Length - 1] == '\\' ?
                path + fileName : path + "\\" + fileName;
            return fullName;
        }

        private string GetFileNameFromPath(string path)
        {
            int index = path.LastIndexOf('\\');

            string fileName = (index < 0) ?
                path : path.Substring(index + 1);

            return fileName;
        }
    }
}
